# main_backtest.py

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os
import pandas_ta as ta


class DeltaCVDBotBacktester:
    """
    A class to backtest the Delta-CVD Hybrid Model Quantitative Strategy.
    """

    def __init__(self, df, params, costs):
        """
        Initializes the Backtester.

        :param df: Pandas DataFrame with OHLCV, volume_delta, and cvd data.
        :param params: Dictionary of strategy parameters.
        :param costs: Dictionary of trading costs (fees, slippage).
        """
        self.df = df.copy()
        self.params = params
        self.costs = costs
        self.trades = []
        self.position = 0  # 0: no position, 1: long, -1: short
        self.entry_price = 0
        self.stop_loss = 0
        self.take_profit = 0
        self.entry_time = None

    def _apply_slippage(self, price, direction):
        """Applies slippage to the execution price."""
        if direction == 1:  # Buying
            return price * (1 + self.costs['slippage'])
        else:  # Selling
            return price * (1 - self.costs['slippage'])

    def _open_position(self, direction, price, time):
        """Opens a new long or short position."""
        self.position = direction
        self.entry_price = self._apply_slippage(price, direction)
        self.entry_time = time

        atr = self.df.loc[time, 'ATR']

        if direction == 1:  # Long
            sl_price = self.entry_price - atr * self.params['stop_loss_atr_multiplier']
            self.stop_loss = sl_price
            self.take_profit = self.entry_price + (self.entry_price - sl_price) * self.params['risk_reward_ratio']
        else:  # Short
            sl_price = self.entry_price + atr * self.params['stop_loss_atr_multiplier']
            self.stop_loss = sl_price
            self.take_profit = self.entry_price - (sl_price - self.entry_price) * self.params['risk_reward_ratio']

        # print(f"{time} | OPEN {'LONG' if direction==1 else 'SHORT'} at {self.entry_price:.2f} | SL: {self.stop_loss:.2f} | TP: {self.take_profit:.2f}")

    def _close_position(self, exit_price, time):
        """Closes the current position and records the trade."""
        exit_price = self._apply_slippage(exit_price, -self.position)

        # Using Taker fee for both entry and exit as we are using market orders
        fee = self.costs['taker_fee'] * 2

        if self.position == 1:  # Long
            pnl_percent = (exit_price / self.entry_price) - 1
        else:  # Short
            pnl_percent = (self.entry_price / exit_price) - 1

        net_pnl_percent = pnl_percent - fee

        self.trades.append({
            'entry_time': self.entry_time,
            'exit_time': time,
            'entry_price': self.entry_price,
            'exit_price': exit_price,
            'direction': 'LONG' if self.position == 1 else 'SHORT',
            'pnl_percent': net_pnl_percent
        })

        # print(f"{time} | CLOSE at {exit_price:.2f} | PnL: {net_pnl_percent:.4%}")
        self.position = 0
        self.entry_price = 0
        self.entry_time = None

    def run_backtest(self):
        """Main loop to run the backtest."""
        print("Running backtest...")

        # Pre-calculate indicators
        self.df['ATR'] = ta.atr(self.df['high'], self.df['low'], self.df['close'], length=14)
        self.df['EMA_TREND'] = ta.ema(self.df['close'], length=self.params['ema_trend_period'])
        self.df['DELTA_MA_ABS'] = self.df['volume_delta'].abs().rolling(window=self.params['delta_ma_period']).mean()

        # Ensure we have enough data for lookbacks
        start_index = max(self.params['divergence_lookback_period'],
                          self.params['breakout_lookback_period'],
                          self.params['ema_trend_period'])

        for i in range(start_index, len(self.df)):
            current_row = self.df.iloc[i]
            prev_row = self.df.iloc[i - 1]
            current_time = self.df.index[i]

            # --- 1. Check for Exits First ---
            if self.position == 1:  # Long
                if current_row['low'] <= self.stop_loss:
                    self._close_position(self.stop_loss, current_time)
                elif current_row['high'] >= self.take_profit:
                    self._close_position(self.take_profit, current_time)
            elif self.position == -1:  # Short
                if current_row['high'] >= self.stop_loss:
                    self._close_position(self.stop_loss, current_time)
                elif current_row['low'] <= self.take_profit:
                    self._close_position(self.take_profit, current_time)

            # --- 2. Check for Entries if no position is open ---
            if self.position == 0:
                # --- A. Mean Reversion Logic ---
                lookback_df = self.df.iloc[i - self.params['divergence_lookback_period']:i]

                # Bullish Divergence
                if current_row['low'] <= lookback_df['low'].min():
                    prev_low_idx = lookback_df['low'].idxmin()
                    if current_row['cvd'] > self.df.loc[prev_low_idx, 'cvd']:
                        if prev_row['volume_delta'] < 0 and current_row['volume_delta'] > 0:  # Delta flip
                            self._open_position(1, current_row['close'], current_time)
                            continue

                # Bearish Divergence
                if current_row['high'] >= lookback_df['high'].max():
                    prev_high_idx = lookback_df['high'].idxmax()
                    if current_row['cvd'] < self.df.loc[prev_high_idx, 'cvd']:
                        if prev_row['volume_delta'] > 0 and current_row['volume_delta'] < 0:  # Delta flip
                            self._open_position(-1, current_row['close'], current_time)
                            continue

                # --- B. Momentum Breakout Logic ---
                breakout_window = self.df.iloc[i - self.params['breakout_lookback_period']: i]

                # Bullish Breakout
                if current_row['close'] > self.df.loc[current_time, 'EMA_TREND']:
                    if current_row['close'] > breakout_window['high'].max():
                        if current_row['volume_delta'] > (
                                self.df.loc[current_time, 'DELTA_MA_ABS'] * self.params['delta_strength_factor']):
                            self._open_position(1, current_row['close'], current_time)
                            continue

                # Bearish Breakout
                if current_row['close'] < self.df.loc[current_time, 'EMA_TREND']:
                    if current_row['close'] < breakout_window['low'].min():
                        if current_row['volume_delta'] < -(
                                self.df.loc[current_time, 'DELTA_MA_ABS'] * self.params['delta_strength_factor']):
                            self._open_position(-1, current_row['close'], current_time)
                            continue
        print("Backtest finished.")
        return self.get_results()

    def get_results(self):
        if not self.trades:
            print("No trades were made.")
            return None

        trades_df = pd.DataFrame(self.trades)
        trades_df['cumulative_pnl'] = (1 + trades_df['pnl_percent']).cumprod() - 1
        return trades_df

    def plot_results(self, trades_df):
        if trades_df is None or trades_df.empty:
            return

        plt.figure(figsize=(12, 6))
        plt.plot(trades_df['exit_time'], (trades_df['cumulative_pnl'] + 1) * 100)
        plt.title('Cumulative PnL (%)')
        plt.xlabel('Date')
        plt.ylabel('Portfolio Value (%)')
        plt.grid(True)
        plt.show()

    def print_stats(self, trades_df):
        if trades_df is None or trades_df.empty:
            return

        total_trades = len(trades_df)
        wins = trades_df[trades_df['pnl_percent'] > 0]
        losses = trades_df[trades_df['pnl_percent'] <= 0]

        win_rate = len(wins) / total_trades if total_trades > 0 else 0
        total_pnl = trades_df['cumulative_pnl'].iloc[-1]

        avg_win = wins['pnl_percent'].mean()
        avg_loss = losses['pnl_percent'].mean()

        profit_factor = abs(wins['pnl_percent'].sum() / losses['pnl_percent'].sum()) if losses[
                                                                                            'pnl_percent'].sum() != 0 else np.inf

        print("\n--- Backtest Results ---")
        print(f"Total Trades: {total_trades}")
        print(f"Win Rate: {win_rate:.2%}")
        print(f"Total PnL: {total_pnl:.2%}")
        print(f"Profit Factor: {profit_factor:.2f}")
        print(f"Average Win: {avg_win:.4%}")
        print(f"Average Loss: {avg_loss:.4%}")
        print("------------------------\n")


def load_data(file_path):
    """Loads and preprocesses data from a CSV file."""
    print(f"Loading data from {file_path}...")
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"Data file not found at: {file_path}")

    df = pd.read_csv(file_path, header=None)
    df.columns = [
        'open_time', 'open', 'high', 'low', 'close', 'volume',
        'close_time', 'quote_volume', 'count', 'taker_buy_volume',
        'taker_buy_quote_volume', 'ignore'
    ]

    # --- Data Preprocessing ---
    df['open_time'] = pd.to_datetime(df['open_time'], unit='ms')
    df.set_index('open_time', inplace=True)

    # Calculate Volume Delta
    df['volume_delta'] = 2 * df['taker_buy_volume'] - df['volume']

    # Calculate Cumulative Volume Delta (CVD)
    df['cvd'] = df['volume_delta'].cumsum()

    print("Data loaded and preprocessed successfully.")
    return df


if __name__ == '__main__':
    # --- Configuration ---
    SYMBOL = "ETHUSDT"
    TIMEFRAME = "1m"
    YEAR = 2024
    MONTH = 6

    # Note: Adjust the root path to your actual location
    ROOT_PATH = r"F:\personal\binance_klines_with_delta"
    file_path = os.path.join(ROOT_PATH, SYMBOL, TIMEFRAME, f"{SYMBOL}-{TIMEFRAME}-{YEAR}-{str(MONTH).zfill(2)}.csv")

    # --- Strategy Parameters ---
    strategy_params = {
        'divergence_lookback_period': 30,
        'breakout_lookback_period': 20,
        'delta_ma_period': 20,
        'delta_strength_factor': 2.5,
        'risk_reward_ratio': 1.5,
        'stop_loss_atr_multiplier': 2.0,
        'ema_trend_period': 50
    }

    # --- Trading Costs ---
    trading_costs = {
        'taker_fee': 0.0005,  # 0.05%
        'maker_fee': 0.0002,  # 0.02%
        'slippage': 0.0001  # 0.01%
    }

    # --- Run Backtest ---
    data_df = load_data(file_path)

    backtester = DeltaCVDBotBacktester(data_df, strategy_params, trading_costs)

    results_df = backtester.run_backtest()

    if results_df is not None:
        backtester.print_stats(results_df)
        backtester.plot_results(results_df)